package sql

import (
	"database/sql"
	"db_demo/config"
	"db_demo/model"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

// sql.DB是表示连接的数据库对象（结构体实例），它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池，它可以安全地被多个goroutine同时使用。
var db *sql.DB

func init() {
	var err error

	dbConfig := config.GetDBConfig()

	// DSN:Data Source Name
	dsn := "%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True"
	dsn = fmt.Sprintf(dsn, dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.DBName)
	// Open函数可能只是验证其参数格式是否正确，实际上并不创建与数据库的连接。不会校验账号密码是否正确
	// 注意！！！这里不要使用:=，我们是给全局变量赋值，然后在main函数中使用全局变量db
	db, err = sql.Open(dbConfig.DriverName, dsn)
	if err != nil {
		panic(err)
	}
	// 返回的DB对象可以安全地被多个goroutine并发使用，并且维护其自己的空闲连接池。因此，Open函数应该仅被调用一次，很少需要关闭这个DB对象。
	// defer db.Close() // 注意这行代码要写在上面err判断的下面
	err = db.Ping()
	if err != nil {
		panic(err)
	}

	// SetMaxOpenConns设置与数据库建立连接的最大数目。
	// 如果n大于0且小于最大闲置连接数，会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n<=0，不会限制最大开启连接数，默认为0（无限制）。
	db.SetMaxOpenConns(20)

	// SetMaxIdleConns设置连接池中的最大闲置连接数。
	// 如果n大于最大开启连接数，则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n<=0，不会保留闲置连接。
	db.SetMaxIdleConns(10)
}

// QueryUserById 根据 id 查询
func QueryUserById(id int64) *model.User {
	sqlStr := "select id, name, age from user where id=?"
	u := &model.User{}
	// 单行查询db.QueryRow()执行一次查询，并期望返回最多一行结果（即Row）。
	// QueryRow总是返回非nil的值，直到返回值的Scan方法被调用时，才会返回被延迟的错误。（如：未找到结果）
	// 非常重要：确保QueryRow之后调用Scan方法，否则持有的数据库链接不会被释放
	err := db.QueryRow(sqlStr, id).Scan(&u.Id, &u.Name, &u.Age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return u
	}
	return u
}

// QueryUserByName 根据 name 查询
func QueryUserByName(name string) []*model.User {
	users := make([]*model.User, 0)
	sqlStr := "select id, name, age from user where name = ?"
	// 多行查询db.Query()执行一次查询，返回多行结果（即Rows），一般用于执行select命令。
	// 参数args表示query中的占位参数。
	rows, err := db.Query(sqlStr, name)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return users
	}
	// 非常重要：关闭rows释放持有的数据库链接
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		u := &model.User{}
		err := rows.Scan(&u.Id, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return users
		}
		users = append(users, u)
	}
	return users
}

// ----------------插入、更新和删除操作都使用Exec方法。-------------------
// InsertUser 插入数据
func InsertUser(name string, age int8) {
	sqlStr := "insert into user(name, age) values (?,?)"
	// Exec 执行一次命令（包括查询、删除、更新、插入等），返回的Result是对已执行的SQL命令的总结。
	// 参数args表示query中的占位参数。
	ret, err := db.Exec(sqlStr, name, age)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

// UpdateUser 更新数据
func UpdateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

// 删除数据
func DeleteUser(id int64) {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, id)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

// --------------------------------------------------------------------

// --------------------- 预处理 -----------------------------
// 什么是预处理？
// 	普通SQL语句执行过程：
// 		客户端对SQL语句进行占位符替换得到完整的SQL语句。
// 		客户端发送完整SQL语句到MySQL服务端
// 		MySQL服务端执行完整的SQL语句并将结果返回给客户端。
// 	预处理执行过程：
// 		把SQL语句分成两部分，命令部分与数据部分。
// 		先把命令部分发送给MySQL服务端，MySQL服务端进行SQL预处理。
// 		然后把数据部分发送给MySQL服务端，MySQL服务端对SQL语句进行占位符替换。
// 		MySQL服务端执行完整的SQL语句并将结果返回给客户端。
// 为什么要预处理？
// 	优化MySQL服务器重复执行SQL的方法，可以提升服务器性能，提前让服务器编译，一次编译多次执行，节省后续编译的成本。
// 	避免SQL注入问题。

// 	预处理查询示例
func PrepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	// Prepare方法会先将sql语句发送给MySQL服务端，返回一个准备好的状态用于之后的查询和命令。
	// 返回值可以同时执行多个查询和命令。
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	defer rows.Close()
	// 循环读取结果集中的数据
	for rows.Next() {
		var u model.User
		err := rows.Scan(&u.Id, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Println(u)
	}
}

// 预处理插入示例
func PrepareInsertDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("小王子", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("沙河娜扎", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

// -----------------------------------------------------------

// --------------------------------- 事务 ---------------------
// Go语言中使用以下三个方法实现MySQL中的事务操作。
// 开始事务
// 	func (db *DB) Begin() (*Tx, error)
// 提交事务
// 	func (tx *Tx) Commit() error
// 回滚事务
// 	func (tx *Tx) Rollback() error

// 事务操作示例
func TransactionDemo() {
	tx, err := db.Begin() // 开启事务
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	ret1, err := tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	sqlStr2 := "Update user set age=40 where id=?"
	ret2, err := tx.Exec(sqlStr2, 3)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	fmt.Println(affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事务提交啦...")
		tx.Commit() // 提交事务
	} else {
		tx.Rollback()
		fmt.Println("事务回滚啦...")
	}

	fmt.Println("exec trans success!")
}
